'torbjoern 26. august 2010
' Boid code taken from a processing.js example:
'http://processingjs.org/learning/topic/flocking
'which again is based on work by Craig Raynolds: http://www.red3d.com/cwr/steer/gdc99/

'todo: clean creation of critters, a TCritter.create() that inserts into displayobjects and appropriate critterList
'29. august:
'-replaced herbivore & predator grids with a single grid. This allows herbs test intersection to preds.
'probably gives a small speedup
'gave predators a toothy grin. (image)
SuperStrict



Framework brl.GLMax2D
Import brl.random
Import brl.standardio
'import brl.pixmap
Import brl.PngLoader
Import brl.map
'import BRL.Blitz 'for incbin

Include "../gridmap/tgrid.bmx"
Include "../common/tvector.bmx"



'SetGraphicsDriver GLMax2DDriver()
Global xres# = 1024
Global yres# = 768
AppTitle = "critterboids"
Graphics xres,yres,0,0,2


Global gMaxInteractDist# = 100 '50-100 is good, limits view dist of all objs.'
 'could have multiple grids. 1 for sight queries.
Global world_xmin:Int = 0
Global world_ymin:Int = 0
Global world_xmax:Int = xres
Global world_ymax:Int = yres
Local xcells:Int = Floor(xres/gMaxInteractDist)
Local ycells:Int = Floor(yres/gMaxInteractDist)
Global gameObjGrid:TGrid = TGrid.Create(world_xmin,world_ymin,world_xmax,world_ymax,xcells,ycells)  


Global displayObjects:TList = New TList

Global predatorList:TList = New TList
Global herbivoreList:TList = New TList

Global herbivoreCount:Int = 0
Global drawHelp:Int = True
Global drawGridmapDebug:Int = False

Global imgTeethClosed:TImage = LoadImage("teeth_closed.png", MIPMAPPEDIMAGE)
Global imgTeethOpen:TImage = LoadImage("teeth_open.png", MIPMAPPEDIMAGE)
MidHandleImage(imgTeethClosed)
MidHandleImage(imgTeethOpen)
Assert imgTeethClosed, "img not loaded"
Assert imgTeethOpen, "img not loaded"

FlushMouse()

Global tim:Int, oldtim:Int
Global deltaMillis:Int
Local dt#

'Make some herbivores to start off with
For Local i:Int = 0 To 10
	Local newHerbivore:THerbivore = New THerbivore 
	newHerbivore.pos.set( GraphicsWidth()/2+Rand(-5,5), GraphicsHeight()/2+Rand(-5,5) )
	displayObjects.addLast(newHerbivore)
	herbivoreList.addLast(newHerbivore)
Next
'and a lone predator
Local newPred:TPredator = New TPredator 
newPred.pos.set( GraphicsWidth()/2+Rand(-5,5), GraphicsHeight()/2+Rand(-5,5) )
displayObjects.addLast(newPred)
predatorList.addLast(newPred)

While Not AppTerminate() And Not KeyHit(KEY_ESCAPE)
	tim = MilliSecs()
	deltaMillis = tim-oldtim
	dt = deltaMillis / 1000.0
	oldtim = tim
	
	logic(dt)
	draw()
Wend

Function logic(dt#)

	herbivoreCount = 0
	gameObjGrid.initMap() 'do each frame
	
	For Local h:THerbivore = EachIn herbivoreList
		herbivoreCount :+1
		
		h.setBoundSquare(gMaxInteractDist)
		'drawrectopen(h.xmin,h.ymin,h.xmax,h.ymax)
		
		Local otherHerbs:TList = gameObjGrid.getIntersectionsWith(h)
		otherHerbs.addLast(h) 'boids use self also
		
		h.run(otherHerbs,dt)
		h.tryToMultiply(otherHerbs)
		
		Local eatenFood:TFood = h.lookForFood(displayobjects)
		If eatenFood <> Null displayObjects.remove(eatenFood)
	Next
	
	
	For Local pred:TPredator = EachIn predatorList
		'pred.run(predatorList,dt)
		pred.setBoundSquare(gMaxInteractDist)
		
		If pred.isHunting()
			If pred.completedHunt()
				pred.huntingTarget.kill()
				pred.huntingTarget = Null
			EndIf
		Else If pred.isHungry()
			'find closest herbivore to predator
			Local closestHerb:THerbivore = Null
			Local minDist# = 1e6
			For Local herb:THerbivore = EachIn herbivoreList
				Local d# = VectorDistance(pred.pos,herb.pos)
				If d < pred.maximumHuntDist
					If d < minDist
						minDist = d
						closestHerb = herb
					EndIf	
				EndIf
			Next
			
			If closestHerb <> Null
				pred.setTarget(closestHerb)
			Else
				pred.walkabout() 'hasnt got any target
			EndIf
		Else 'not hunting or hungry
			pred.vel.set(0,0)
			pred.acc.set(0,0)
			
		EndIf

		pred.updateLife(dt)
		pred.update(dt)
	Next
	
End Function

Function draw()

	For Local drawable:IDrawable = EachIn displayObjects
		drawable.draw()
	Next
	
	If drawGridmapDebug
		gameObjGrid.drawGrid()
		gameObjGrid.drawConnections()
	EndIf
	
	If MouseHit(1)
		Local newPred:TPredator = New TPredator
		newPred.pos.set( MouseX(), MouseY() )
		displayObjects.addLast( newPred )
		predatorList.addLast( newPred )
	EndIf
	
	If MouseHit(2)
		For Local i:Int = 0 Until 10
		Local newHerbivore:THerbivore = New THerbivore 
		newHerbivore.pos.set( MouseX()+Rand(-5,5), MouseY() )
		displayObjects.addLast(newHerbivore)
		herbivoreList.addLast(newHerbivore)
		Next
	EndIf
	
	If MouseHit(3)
		Local newFood:TFood = New TFood 
		newFood.pos.set( MouseX(), MouseY() )
		displayObjects.addLast( newFood)
	EndIf
	
	If KeyHit(KEY_H)
		drawHelp = Not drawHelp
	EndIf
	
	If KeyHit(KEY_1)
		drawGridmapDebug = Not drawGridmapDebug
	EndIf
	
	
	'Reset. Remove all critters.
	If KeyHit(KEY_R)
		predatorList.clear()
		herbivoreList.clear()
		For Local o:THerbivore = EachIn displayObjects
			displayObjects.remove(o)
		Next
		
		For Local o:TPredator = EachIn displayObjects
			displayObjects.remove(o)
		Next
		
		gameObjGrid.boxes.clear()
	EndIf
	
	
	SetColor 196,196,196
	If drawHelp
		DrawText "herbivoreCount: " + herbivoreCount, 15,15*1
		DrawText "add predator - mouse 1", 15, 15*2
		DrawText "add 10 herbivores - mouse 2", 15, 15*3
		DrawText "place 1 food - mouse 3", 15, 15*4
		DrawText "herbivores multiply if they have eaten food and collide with another one", 15, 15*5
		DrawText "predators... are a bit daft when they are hunting prey...", 15, 15*6
		DrawText "deltaMillis "+deltaMillis ,15,15*7
		DrawText "press 1 to show herbivore gridmap debuginfo ",15,15*8
	Else
		DrawText "show help - H",15,15
	EndIf
	
	Flip()
	If deltamillis < 8 Delay 8
	Cls()
	
End Function

Type IDrawable Extends TBoundingBox
	 Method draw() Abstract
End Type

Type Critter Extends IDrawable
	Field pos:TVector = New TVector
	Field vel:TVector = New TVector
	Field acc:TVector = New TVector
	
	Field health:Int = 100
	
	Method setBoundSquare(s#)
		Local sh# = s*.5
		setBounds( pos._x-sh, pos._y-sh, pos._x+sh, pos._y+sh )
	End Method
End Type

Type TBoid Extends Critter
	Field r#
	Field maxforce#
	Field maxspeed#
	
	Method New()
		r = 3.0
		vel.set( Rnd(-1,1), Rnd(-1,1) )
		maxforce = 0.005'0.05 'Maximum steering force  
	End Method
	
	Method run(otherboids:TList,dt#)
		flock(otherboids)
		update(dt)
	End Method
	
	'accumulate a new accel
	Method  flock(otherboids:TList)
		Local sep:TVector = separate(otherboids)
		Local ali:TVector = align(otherboids)
		Local coh:TVector = cohesion(otherboids)
		'Arbitrarily weight these forces  
		sep.mult(10.0)  
		ali.mult(5.0)  
		coh.mult(5.0)  
		'Add the force vectors To acceleration  
		acc.add(sep)
		acc.add(ali) 
		acc.add(coh) 
		acc.add( bounceScreenEdges() ) 
	End Method
	
	'update location
	Method update(dt#)
		vel.add(acc)
		vel.limit(maxspeed)
		pos.add(vel)
		acc.set(0,0) 'reset accel each cycle
	End Method
	
	Method seek(target:TVector)
		acc.add( steer(target, False) )
	End Method
	
	Method flee:TVector(target:TVector)
		Local seekAccel:TVector = steer(target, False)
		seekAccel._x :* -1
		seekAccel._y :* -1
		Return seekAccel
	End Method
	
	Method arrive(target:TVector)
		acc.add( steer(target, True) )
	End Method	

	'A method that calculates a steering vector towards a target  
	'Takes a second argument, If True, it slows down as it approaches the target  
	Method steer:TVector(target:TVector, slowdown:Int)
		Local steer:TVector = Null
		Local desired:TVector = VectorSub(target,pos)
		Local d# = desired.magnitude()
		If d > 0
			desired.normalize()
			
			'To options for desired vector magnitude.
			If( slowdown And d < 100.0 )
				desired.mult(maxspeed*d/100) '1.based on distance
			Else
				desired.mult(maxspeed) '2.based on maxspeed
			EndIf
			
			steer = vectorsub(desired,vel) 'steering minus velocity
			steer.limit(maxforce)
		Else
			steer = VectorCreate(0,0)
		EndIf
		Return steer
	End Method
	
	'Method checks for nearby boids and steers away  
	Method separate:TVector(otherboids:TList)
		Local desiredSeparation# = 25.0
		Local sum:TVector = VectorCreate(0,0)
		Local count:Int = 0
		'for every boid in the system, check if its too close
		For Local other:TBoid = EachIn otherboids
			'If other=Self Continue
			Local d# = VectorDistance(pos,other.pos)
			If d > 0 And d < desiredSeparation
				'Calculate vector pointing away from neighbor
				Local diff:TVector = VectorSub(pos, other.pos)
				diff.normalize()
				diff.div(d) 'weight by distance
				sum.add(diff)
				count :+1 'keep track of how many are too close
			EndIf
		Next
		
		'average - divide by how many
		If count > 0
			sum.div(Float(count))
		EndIf
		Return sum
	End Method
	
	'For every nearby boid in the system, 
	'calculate the average velocity  
	Method align:TVector(otherboids:TList)
		Local neighborDist# = 50.0
		Local sum:TVector = VectorCreate(0,0)
		Local count:Int = 0
		For Local other:TBoid = EachIn otherboids
			'If other=Self Continue

			Local d# = VectorDistance(pos,other.pos)
			If d > 0 And d < neighborDist
				sum.add(other.vel)
				count :+1
			EndIf
		Next
		
		If count > 0
			sum.div(Float(count))
			sum.limit(maxforce)
		EndIf
		
		Return sum		
	End Method
	
	'For the average location (i.e. center) of all nearby boids,
	'calculate steering vector towards that location  
	Method cohesion:TVector(otherboids:TList)
		Local neighborDist# = 80.0
		Local sum:TVector = VectorCreate(0,0)
		Local count:Int = 0
		For Local other:TBoid = EachIn otherboids
			'If( other=Self ) Continue

			Local d# = VectorDistance(pos,other.pos)
			If d > 0 And d < neighborDist
				sum.add(other.pos)
				count :+1
			EndIf
		Next
		
		If count > 0
			sum.div(Float(count))
			Return steer(sum, False) 'steer towards loc
		EndIf
		
		Return sum
	End Method
	
	
	Method bounceScreenEdges:TVector()
		Local edgeLim# = 50.0
		Local acc:TVector = VectorCreate(0,0)

		If(pos._x < world_ymin+edgeLim) acc._x = pos._x/edgeLim
		If(pos._x > world_xmax-edgeLim) acc._x = -pos._x/edgeLim
		If(pos._y < world_ymin+edgeLim) acc._y = pos._y/edgeLim
		If(pos._y > world_ymax-edgeLim) acc._y = -pos._y/edgeLim
				
		Return acc
	End Method
End Type

Type TPredator Extends TBoid
	Field huntCooldown# 'seconds
	Field hasEaten:Int = False
	Field randomWanderTarget:TVector = VectorCreate( Rand(10,GraphicsWidth()), Rand(10,GraphicsHeight()))
	Field teethOpen:Int = False
	Field openTimer# = 0
	
	Field huntingTarget:THerbivore = Null
	
	Method New()
		maxspeed = 1.0
		gameObjGrid.insertBox(Self)
		
		maxspeed = 8.0
		huntCooldown = 0.0 'hungry! 
	End Method

	Field siz# = 20
	Field maximumHuntDist# = 80
	
	Method draw()
		Local hunting:Int = False
		
		SetColor 255,255,255
		If huntCooldown < .1
			hunting=True
		EndIf
		
		If huntCooldown < 0
			siz# = 5
		Else If huntCooldown > 0
			siz# = 5+20*huntCooldown/5.0
		EndIf
		
		
		'DrawRect( pos._x-siz/2, pos._y-siz/2, siz,siz )
		'drawcircle( pos._x, pos._y, maximumHuntDist, 18)
		SetScale .25, .25
		
		If hunting
			SetRotation vel.heading2D()

			DrawImage imgTeethClosed, pos._x, pos._y			

			SetRotation 0
		Else
			DrawImage imgTeethOpen, pos._x, pos._y
			

			If teethOpen
				DrawImage imgTeethClosed, pos._x, pos._y			
			Else
				DrawImage imgTeethOpen, pos._x, pos._y
			EndIf
		EndIf
		
		SetScale 1,1
		
		If huntCoolDown < 0
			'DrawLine pos._x, pos._y, pos._x+vel._x*10, pos._y+vel._y*10
		Else
			SetColor 255,255,255
			DrawText "om nom nom nom",pos._x-5*10, pos._y-15+Rand(-1,1)
		EndIf
		
		If gotTarget()
			SetColor 255,255,255
			DrawText "groar!",pos._x+Rand(-1,1), pos._y-15+Rand(-1,1)
			DrawLine pos._x, pos._y, huntingTarget.pos._x, huntingTarget.pos._y
		EndIf
		
		If hasEaten 
			hasEaten = False
		EndIf
	End Method
	
	Method updateLife(dt#)
		openTimer :+ dt
		If openTimer > .5
			openTimer = 0
		EndIf
		
		teethOpen = True
		If openTimer < .25 teethOpen = False
	
		huntCooldown :- dt
	End Method
	
	Method eat()
		health = 100
		huntCooldown = 5.0
		hasEaten = True
	End Method
	
	Method isHungry:Int()
		Return huntCooldown < 0
	End Method
	
	Method isHunting:Int()
		Return gotTarget()
	End Method
	
	Method gotTarget:Int()
		Return huntingTarget <> Null
	End Method
	
	Method setTarget(herb:THerbivore)
		huntingTarget = herb
	End Method
	
	Method completedHunt:Int()
		Local huntComplete:Int = False
		Local targetNotNull:Int = False
		
		maxspeed = 10
		maxforce = 0.1
		
		arrive(huntingTarget.pos)

		Local distToTarget# = VectorDistance(pos, huntingTarget.pos)
		Local inRange:Int = distToTarget < (2*r + 2*huntingTarget.r)
		
		If inRange
			eat()
			huntComplete = True
		EndIf
		
		Return huntComplete
	End Method
	
	Method walkabout()
		
		maxspeed = 3
		maxforce = 0.05
	
		If huntCoolDown > 0 Return ' not hungry
		arrive(randomWanderTarget)
		Local targDist# = VectorDistance(pos, randomWanderTarget) 
		If targDist < 2*r
			randomWanderTarget.set( Rand(10,GraphicsWidth()), Rand(10,GraphicsHeight()))
		EndIf
		
	End Method
End Type

Type THerbivore Extends TBoid
	Field hasEaten:Int = False
	Field panic:Int = False

	Method New()
		maxspeed = 1.0
		gameObjGrid.insertBox(Self)
		Self.setBounds( pos._x-r, pos._y-r, pos._x+r, pos._y+r )
	End Method
	
	Method kill()
		herbivoreList.remove(Self)
		displayObjects.remove(Self)
		gameObjGrid.removeBox(Self)
	End Method
	
	Method  flock(otherboids:TList)
		Global numsprinted:Int = 0
		
		If numsprinted = 0
			numsprinted = 1
			Print "THerbivore.flock"
		EndIf
		
		Local sep:TVector = separate(otherboids)
		Local ali:TVector = align(otherboids)
		Local coh:TVector = cohesion(otherboids)
		Local predAvoidance:TVector = avoidPredators()
		'Arbitrarily weight these forces  
		sep.mult(10.0)  
		ali.mult(15.0)  
		coh.mult(5.0)  
		predAvoidance.mult(100.0)
		'Add the force vectors To acceleration  
		acc.add(sep)
		acc.add(ali) 
		acc.add(coh) 
		acc.add(predAvoidance)
		acc.add( bounceScreenEdges() ) 
	End Method
	
	Method avoidPredators:TVector()
		'setBoundSquare(60)
			Local predsInRange:TList = gameObjGrid.getIntersectionsWith(Self)
		
			Local predCount:Int = 0 
			Local sum:TVector = New TVector
			For Local pred:TPredator = EachIn predsInRange
				If( VectorDistanceSquared(pred.pos, Self.pos) < 70*70 )
					predCount:+1
					sum.add( flee(pred.pos) )
				EndIf
			Next
			If predCount > 0
				Self.panic = True
				sum.div(predCount)
			Else
				Self.panic = False
			EndIf
			
			Return sum
	End Method

	Method draw()
		If hasEaten 
			SetColor 255,255,0
		Else
			SetColor 0,255,0
		EndIf
		Local theta# = vel.heading2D() + 90
		SetOrigin pos._x, pos._y
		SetRotation theta
				
		Local poly:Float[] = [0.0,-2*r,..
					     -r,r*2,..
					     r,r*2]

		DrawPoly(poly)
		SetRotation 0
		SetOrigin 0,0
		
		
		
		If panic
			SetColor 255,255,255
			SetScale .8,.8
			DrawText "squeek",pos._x, pos._y-15
			SetScale 1,1
		EndIf

	End Method
	
	Method lookForFood:TFood(foodlist:TList)
		If hasEaten Return Null
		
		Local eatenFood:TFood = Null
		
		Local closestObj:TFood
		Local minDist# = 1e6
		For Local food:TFood = EachIn foodlist
			Local d# = VectorDistance(Self.pos,food.pos)
			If d < minDist
				minDist = d
				closestObj = food
			EndIf	
		Next
		
		If closestObj <> Null
			If minDist < (r + closestObj.r)
				eatenFood = closestObj
				hasEaten = True
			EndIf
		EndIf
	
		Return eatenFood
	End Method
	
	Method tryToMultiply(herblist:TList)
		If(hasEaten = False) Return
	
		Local closestHerb:THerbivore = Null
		Local minDist# = 1e6
		For Local herb:THerbivore = EachIn herblist
			If herb=Self Continue
			Local d# = VectorDistance(Self.pos,herb.pos)
			If d < minDist
				minDist = d
				closestHerb = herb
			EndIf	
		Next
		
		If closestHerb <> Null And minDist < r*10
			'If( Rnd(0,1) < .5)
			If True
				hasEaten = False
				Local offspring:THerbivore = New THerbivore
				offspring.pos.copy( Self.pos )
				herblist.addLast( offspring)
			EndIf
		EndIf
		
	End Method
End Type

Type ICritterInteractable Extends IDrawable
	Field pos:TVector = New TVector
	Field r#
End Type

Type TFood Extends ICritterInteractable
	Method New()
		r = 30
	End Method

	Method draw()
		SetColor 255,255,0
		DrawOval( pos._x-r/2, pos._y-r/2, r,r )
	End Method	
End Type

Function drawcircle(x#,y#,r#,segs:Int)
	Local oldpx# = x+Cos(0)*r
	Local oldpy# = y+Sin(0)*r
	For Local i:Int = 0 To segs
		Local a# = i*360.0 / Float(segs)
		Local xp# = x+Cos(a)*r
		Local yp# = y+Sin(a)*r
		DrawLine(oldpx,oldpy,xp,yp)
		oldpx = xp
		oldpy = yp
	Next
End Function

